package crp.core.security.domain.services;

import com.google.common.base.Objects;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.qwlabs.lang.C2;
import crp.core.security.domain.models.RoleEntity;
import crp.core.security.domain.models.UserPermissions;
import crp.core.security.domain.models.UserRoleEntity;
import crp.core.security.domain.repositories.RoleRepository;
import crp.core.user.domain.repositories.UserRepository;
import crp.core.security.domain.repositories.UserRoleRepository;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
import javax.transaction.Transactional;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@ApplicationScoped
public class PermissionLoader {
    private final UserRepository userRepository;
    private final UserRoleRepository userRoleRepository;
    private final RoleRepository roleRepository;
    private final Instance<UserRoleAugmentor> userRoleAugmentor;

    @Inject
    public PermissionLoader(UserRepository userRepository,
                            UserRoleRepository userRoleRepository,
                            RoleRepository roleRepository,
                            Instance<UserRoleAugmentor> userRoleAugmentor) {
        this.userRepository = userRepository;
        this.userRoleRepository = userRoleRepository;
        this.roleRepository = roleRepository;
        this.userRoleAugmentor = userRoleAugmentor;
    }

    @Transactional
    public Stream<UserPermissions> load(Set<String> userIds) {
        Map<String, Set<String>> userRoles = this.loadUserRoles(userIds);
        Map<String, Set<String>> rolePermissions = this.loadRolePermissions();
        return userIds.stream().map(userId -> {
            Set<String> permissions = userRoles.getOrDefault(userId, Set.of()).stream()
                    .map(roleId -> rolePermissions.getOrDefault(roleId, Set.of()))
                    .flatMap(Set::stream)
                    .collect(Collectors.toSet());
            return UserPermissions.builder()
                    .userId(userId)
                    .permissions(permissions)
                    .build();
        });
    }

    @Transactional
    public Set<String> load(String userId) {
        return this.load(Set.of(userId))
                .filter(up -> Objects.equal(up.getUserId(), userId))
                .map(UserPermissions::getPermissions)
                .findFirst()
                .orElseGet(Set::of);
    }

    //    @CacheResult(cacheName = CacheNames.ROLES_ALL)
    protected Map<String, Set<String>> loadRolePermissions() {
        Stream<RoleEntity> roles = roleRepository.findAll().stream();
        return C2.map(roles, RoleEntity::getId, role -> C2.set(role.getPermissions()));
    }

    private Map<String, Set<String>> loadUserRoles(Set<String> userIds) {
        Map<String, Set<String>> userRoles = Maps.newHashMap();
        loadFixedUserRoles(userIds).forEach((userId, roleIds) ->
                userRoles.computeIfAbsent(userId, Sets::newHashSet).addAll(roleIds));
        loadAugmentUserRoles(userIds).forEach((userId, roleIds) ->
                userRoles.computeIfAbsent(userId, Sets::newHashSet).addAll(roleIds));
        return userRoles;
    }

    private Map<String, Set<String>> loadFixedUserRoles(Set<String> userIds) {
        List<UserRoleEntity> userRoles = C2.list(userRoleRepository.findByUserIds(userIds));
        return C2.setMap(userRoles, UserRoleEntity::getUserId,
                UserRoleEntity::getRoleId);
    }

    private Map<String, Set<String>> loadAugmentUserRoles(Set<String> userIds) {
        Map<String, Set<String>> userRoles = Maps.newHashMap();
        userRepository.findByIds(userIds).forEach(user -> {
            Set<String> roleIds = userRoleAugmentor.stream()
                    .flatMap(augmentor -> augmentor.augment(user))
                    .map(RoleEntity::getId)
                    .collect(Collectors.toSet());
            userRoles.put(user.getId(), roleIds);
        });
        return userRoles;
    }

}
